// Copyright (c) 2023 Presto Labs Pte. Ltd.
// Author: qiang

#include "coin2/exchange/mercado/order_v4/parser.h"

#include "coin2/exchange/mercado/api_util/common.h"
namespace coin2::exchange::mercado::order_v4 {

using JsonValue = rapidjson::GenericValue<rapidjson::UTF8<>>;
bool MercadoParser::IsMercadoSpotErrMessage(const JsonValue& doc) {
  if (doc.HasMember("code") && doc.HasMember("message")) {
    return true;
  }

  return false;
}

std::string MercadoParser::GetExchangeOrderId(
    const std::string_view& symbol,
    const std::string_view& exchange_order_id) {
  return fmt::format("{}-{}", symbol, exchange_order_id);
}

int64_t MercadoParser::GetTimestamp(const std::string& time_str) {
  return stoll(time_str) * 1000000;
}

int64_t MercadoParser::GetProcOrderId(const JsonValue& data) {
  if (data.HasMember("externalId") && data["externalId"].IsString()) {
    if (!data["externalId"].IsNull()) {
      return stoll(data["externalId"].GetString());
    }
  }
  return 0;
}

coin::proto::OrderSide MercadoParser::GetOrderSide(const std::string& side) {
  if (side == "buy") {
    return coin::proto::OrderSide::BUY_ORDER;
  } else if (side == "sell") {
    return coin::proto::OrderSide::SELL_ORDER;
  } else {
    return coin::proto::OrderSide::UNKNOWN_ORDER_SIDE;
  }
}

coin::proto::OrderType MercadoParser::GetOrderType(const std::string& type) {
  if (type == "limit") {
    return coin::proto::OrderType::LIMIT_ORDER;
  } else if (type == "market") {
    return coin::proto::OrderType::MARKET_ORDER;
  } else {
    return coin::proto::OrderType::UNKNOWN_ORDER_TYPE;
  }
}

void MercadoParser::SetOrderStatus(ProductOrderElement* order, const std::string& status) {
  if (status == "working" || status == "pending" || status == "created") {
    order->set_is_live(true);
  } else if (status == "cancelled") {
    order->set_is_live(false);
    order->set_cancel_confirmed(true);
  } else if (status == "filled") {
    order->set_is_live(false);
    order->set_fully_filled(true);
  } else {
    LOG(ERROR) << "[SetOrderStatus] Unknown order status: " << status;
  }
}

void MercadoParser::ParseListOrder() {
  /*
  [
    ...
  ] */
  JsonDocument doc;
  if (!PrepareJsonDocument(&doc)) {
    LOG(ERROR) << "[ParseListOrder] Parsing error\n" << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (!doc.IsArray()) {
    LOG(ERROR) << "[ParseListOrder] Invalid data format\n" << JsonToString(doc);
    return;
  }

  result_.mutable_order_list()->Clear();
  result_.mutable_fill_list()->Clear();

  for (const auto& data : doc.GetArray()) {
    std::string status = data["status"].GetString();
    ParseOrderInfo(data);
  }
  if (result_.order_list().each_order_size()) {
    result_.set_type(MsgType::ORDER_STATUS);
  }

  if (result_.fill_list().each_fill_size()) {
    result_.set_type(MsgType::ORDER_FILL);
  }
}

void MercadoParser::ParseOrderInfo(const rapidjson::GenericValue<rapidjson::UTF8<>>& data) {
  /*
    {
        "avgPrice": 500,
        "cost": 10000,
        "created_at": 1636047578,
        "executions":
      [
        {
            "executed_at": 1634731027,
            "fee_rate": "0.70000000",
            "id": "16",
            "instrument": "BTC-BRL",
            "price": 500,
            "qty": "0.001",
            "side": "buy"
        }
      ],
        "externalId": "1372183",
        "fee": "0.003",
        "filledQty": "0.001",
        "id": "100",
        "instrument": "BTC-BRL",
        "limitPrice": 9997,
        "qty": "0.01000000",
        "side": "buy",
        "status": "filled",
        "stopPrice": 18000,
        "triggerOrderId": "42",
        "type": "limit",
        "updated_at": 1636047578
        }
  */

  auto order = result_.mutable_order_list()->add_each_order();
  order->set_market_type(MarketType::Spot);
  order->set_exchange_type(ExchangeType::Mercado);
  order->set_api_version("v4");
  order->set_exchange_order_id(
      GetExchangeOrderId(data["instrument"].GetString(), data["id"].GetString()));
  order->set_symbol(data["instrument"].GetString());
  order->set_proc_order_id(GetProcOrderId(data));
  order->set_price(stod(data["limitPrice"].GetString()));
  order->set_qty(stod(data["qty"].GetString()));
  order->set_side(GetOrderSide(data["side"].GetString()));
  order->set_order_type(GetOrderType(data["type"].GetString()));
  order->set_timestamp(GetCurrentTimestamp());
  order->set_created_timestamp(std::stoll(data["created_at"].GetString()) * 1000'000'000LL);

  SetOrderStatus(order, data["status"].GetString());
  ParseFilledExecutions(data);
}

AccountBalance MercadoParser::ParseBalance(const JsonValue& doc) {
  RawBalanceMap raw_balance_map;
  bool success = false;
  success = ParseRawBalance(doc, &raw_balance_map);
  if (!success) {
    return {};
  }

  AccountBalance acc_balance;
  acc_balance.set_exchange("Mercado");
  acc_balance.set_market_type("Spot");
  for (const auto& native_currency : symcache_->GetCurrenciesNativeUpper()) {
    auto* balance = acc_balance.add_each_balance();
    if (raw_balance_map.count(native_currency) == 0) {
      balance->set_total(0);
      balance->set_hold(0);
      balance->set_available(0);
    } else {
      balance->MergeFrom(raw_balance_map.at(native_currency));
    }
    auto* currency = symcache_->GetCurrencyByNative(native_currency);
    balance->set_currency(currency->currency());
    balance->set_currency_native(currency->native_currency());
    DLOG(INFO) << "BALANCE: " << currency->native_currency() << " " << balance->total() << " "
               << balance->available() << " " << balance->hold();
  }

  return acc_balance;
}

bool MercadoParser::ParseRawBalance(const JsonValue& doc, RawBalanceMap* raw_balance_map) {
  /*
[
    {
        "available": "1000.00000050",
        "on_hold": "300.00000300",
        "symbol": "BRL",
        "total": "1300.00000350"
    }
]
  */

  if (!doc.IsArray()) {
    LOG(ERROR) << "[ParseBalance] Invalid data format\n" << JsonToString(doc);
    return false;
  }

  for (const auto& data : doc.GetArray()) {
    if (!data.HasMember("available") || !data.HasMember("symbol") || !data.HasMember("total")) {
      continue;
    }
    CurrencyBalance currency_balance;
    currency_balance.set_currency_native(data["symbol"].GetString());
    auto total = stod(data["total"].GetString());
    auto avail = stod(data["available"].GetString());
    currency_balance.set_available(avail);
    currency_balance.set_total(total);
    currency_balance.set_hold(total - avail);
    raw_balance_map->emplace(currency_balance.currency_native(), currency_balance);
  }

  return true;
}

void MercadoParser::ParseFilledExecutions(const JsonValue& doc) {
  /*
  "executions": [
    {
      "id": "5124464",
      "instrument": "ETH-BRL",
      "price": 8487.92642001,
      "qty": "0.00100000",
      "side": "sell",
      "fee_rate": "0.70000000",
      "executed_at": 1675762935
    }
  ]
  */

  if (!doc.HasMember("executions")) {
    return;
  }

  std::string native_symbol = doc["instrument"].GetString();
  if (!symcache_->has_product_native(native_symbol)) {
    return;
  }

  auto* product = symcache_->GetProductByNative(native_symbol);

  auto product_fill_bundle = result_.mutable_fill_list();
  for (const auto& info : doc["executions"].GetArray()) {
    auto fill = product_fill_bundle->add_each_fill();
    fill->set_symbol(info["instrument"].GetString());
    fill->set_fill_timestamp(std::stoll(info["executed_at"].GetString()) * 1000'000'000LL);
    fill->set_exchange_order_id(
        GetExchangeOrderId(doc["instrument"].GetString(), doc["id"].GetString()));
    fill->set_price(stod(info["price"].GetString()));
    fill->set_qty(stod(info["qty"].GetString()));
    fill->set_fill_id(product->symbol() + info["id"].GetString());

    double fee_rate = stod(info["fee_rate"].GetString()) / 100;
    auto const side = std::string(info["side"].GetString());
    if (side == "buy") {
      double fill_fee = fee_rate * fill->qty();
      fill->set_fee_currency(product->base().symbol());
      fill->set_fee(fill_fee);
    } else if (side == "sell") {
      double fill_fee = fee_rate * fill->price() * fill->qty();
      fill->set_fee_currency(product->quote().symbol());
      fill->set_fee(fill_fee);
    } else {
      LOG(ERROR) << "unknown side value: " << side;
    }
  }
}

void MercadoParser::ParseSubmitResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::SUBMIT_RESPONSE);
  auto resp = result_.mutable_submit_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_timestamp(GetCurrentTimestamp());
  resp->set_success(false);

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseSubmitResponse] AsioHttpContextStatus error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    return;
  }

  const auto http_code = http_context->res.result_int();
  if (http_code == 429) {
    LOG(ERROR) << "[ParseSubmitResponse] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_exchange_error_msg(http_context->res.body().data());
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[ParseSubmitResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("code") && json.HasMember("message")) {
    auto error_code = GetOrderErrorCode(json["code"], json["message"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(json));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[ParseSubmitResponse] Error response\n" << http_context->res;
      return;
    }
  }

  if (json.HasMember("orderId")) {
    resp->set_exchange_order_id(
        GetExchangeOrderId(order_context->native_product(), json["orderId"].GetString()));
  } else {
    LOG(ERROR) << "[ParseSubmitResponse] Invalid data format\n" << JsonToString(json);
    return;
  }

  resp->set_success(true);
}

void MercadoParser::ParseRestSpotAccountBalance() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseRestSpotAccountBalance] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  auto account_balance = ParseBalance(document);
  if (!account_balance.has_exchange() || !account_balance.has_market_type()) {
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  result_.set_type(MsgType::ACCOUNT_BALANCE);
  *result_.mutable_account_balance() = std::move(account_balance);
}

void MercadoParser::ParseCancelResponse() {
  const auto order_context = msg_->oc;
  const auto& http_context = msg_->http_context;
  if (!IsValidResponse(http_context)) {
    return;
  }

  result_.set_type(MsgType::CANCEL_RESPONSE);
  auto resp = result_.mutable_cancel_response();
  resp->Clear();
  resp->set_symbol(order_context->order_spec().product);
  resp->set_proc_order_id(order_context->proc_order_id());
  resp->set_http_status(http_context->res.result_int());
  resp->set_exchange_order_id(order_context->exchange_order_id());
  resp->set_timestamp(GetCurrentTimestamp());

  if (http_context->status == AsioHttpContextStatus::kError) {
    LOG(ERROR) << "[ParseCancelResponse] AsioHttpContextStatus error";
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_NETWORK_ERROR);
    resp->set_success(false);
    return;
  }

  const auto http_code = http_context->res.result_int();
  if (http_code == 429) {
    LOG(ERROR) << "[ParseCancelResponse] API_LIMIT_ERROR";
    resp->set_error_code(coin::proto::OrderErrorCode::API_LIMIT_ERROR);
    resp->set_exchange_error_msg(http_context->res.body().data());
    resp->set_success(false);
    return;
  }

  JsonDocument json;
  if (!PrepareJsonDocument(&json)) {
    LOG(ERROR) << "[ParseCancelResponse] Parsing error\n" << http_context->res;
    resp->set_error_code(coin::proto::OrderErrorCode::ORDER_API_ERROR);
    resp->set_success(false);
    return;
  }

  if (json.HasMember("code") && json.HasMember("message")) {
    auto error_code = GetOrderErrorCode(json["code"], json["message"]);
    resp->set_error_code(error_code);
    resp->set_exchange_error_code(GetExchangeOrderErrorCode(json));
    resp->set_exchange_error_msg(GetExchangeOrderErrorMsg(json));
    if (error_code != coin::proto::OrderErrorCode::ORDER_NO_ERROR) {
      LOG(ERROR) << "[ParseCancelResponse] Error response\n" << http_context->res;
      resp->set_success(false);
      return;
    }
  }

  resp->set_success(true);
  resp->set_cancel_confirmed(true);
}

void MercadoParser::ParseWsMessage() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseWsMessage] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }
  DLOG(INFO) << "OG ws msg: " << JsonToString(document);
  // websocket unsupported
}

void MercadoParser::ParseAccounts() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseParseAccounts] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (!document.IsArray()) {
    LOG(ERROR) << "[ParseParseAccounts] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  for (const auto& info : document.GetArray()) {
    if (std::string(info["currency"].GetString()) == "BRL") {
      std::string id = info["id"].GetString();
      mb_->PublishEvent("mercado/accounts", &id);
    } else {
      LOG(INFO) << "ignorn account " << JsonToString(info);
    }
  }
}

void MercadoParser::ParseToken() {
  JsonDocument document;
  if (!PrepareJsonDocument(&document)) {
    LOG(ERROR) << "[ParseParseToken] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  if (!document.HasMember("access_token") && !document.HasMember("expiration")) {
    LOG(ERROR) << "[ParseParseToken] Parsing Error. " << msg_->DebugString();
    result_.set_error_code(OrderErrorCode::INVALID_JSON);
    return;
  }

  struct token tn;
  tn.token_str = document["access_token"].GetString();
  tn.expiration = stoll(document["expiration"].GetString()) * 1e9;
  mb_->PublishEvent("meradobitcoin/token", &tn);
}

auto MercadoParser::ParseRawMessage(const RawMessage& msg) -> const ParseResult& {
  result_.Clear();
  msg_ = &msg;
  sub_topic_id_ = static_cast<OrderSubTopicId>(msg_->type);

  if (msg.type == OrderSubTopicId::WS_MESSAGE) {
    ParseWsMessage();
    return result_;
  }

  CHECK(msg.type == OrderSubTopicId::REST_MESSAGE);
  switch (msg.http_context->query_type) {
    case OrderSubTopicId::REST_ACCOUNTS:
      ParseAccounts();
      break;
    case OrderSubTopicId::LISTEN_KEY:
      ParseToken();
      break;
    case OrderSubTopicId::SUBMIT_RESPONSE:
      ParseSubmitResponse();
      break;
    case OrderSubTopicId::CANCEL_RESPONSE:
      ParseCancelResponse();
      break;
    case OrderSubTopicId::REST_ACCOUNT_BALANCE:
      ParseRestSpotAccountBalance();
      break;
    case OrderSubTopicId::REST_ALL_ORDERS:
      ParseListOrder();
      break;
    default: {
      LOG(ERROR) << "[ParseRawMessage] Unknown query type\n" << msg_->DebugString();
      result_.set_error_code(OrderErrorCode::ORDER_UNKNOWN_ERROR);
      break;
    }
  }

  return result_;
}

}  // namespace coin2::exchange::mercado::order_v4
